SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
12.7 KB · 297 lines tsx
Raw Blame History
1import Link from "next/link";2import { notFound } from "next/navigation";3import { ArrowLeft } from "lucide-react";4import { requireAdmin } from "@/lib/session";5import { getOrganizationDetail } from "@/lib/queries/admin";6import { formatBytes, formatDate, formatMs, formatNumber, formatPercent, formatUsd } from "@/lib/format";7import { PageHeader } from "@/components/ui/page-header";8import { Badge, StatusBadge } from "@/components/ui/badge";9import { Button } from "@/components/ui/button";10import { Stat, StatGrid } from "@/components/ui/stat";11import { Table, TableBody, TableCell, TableEmpty, TableHead, TableHeader, TableRow } from "@/components/ui/table";12import { CopyButton } from "@/components/ui/copy-button";13import { Alert } from "@/components/ui/alert";14import { DateCell, KV, MarginText, Mono, NetworkBadge, Panel, PlanBadge, numCell } from "@/components/admin/primitives";15import { OrgLimitsForm, OrgProviderVisibilitySwitch, OrgSuspendSwitch } from "@/components/admin/org-actions";1617export const dynamic = "force-dynamic";1819export default async function AdminOrganizationDetail({ params }: { params: Promise<{ id: string }> }) {20  await requireAdmin();21  const { id } = await params;22  const d = await getOrganizationDetail(id);23  if (!d) notFound();24  const { org, stats30d: s } = d;25  const margin = s.revenue - s.cost;2627  return (28    <>29      <div className="mb-4">30        <Button asChild variant="ghost" size="sm" className="-ml-2">31          <Link href="/admin/organizations">32            <ArrowLeft className="size-3.5" /> Organizations33          </Link>34        </Button>35      </div>36      <PageHeader37        eyebrow="Organization"38        title={39          <span className="flex flex-wrap items-center gap-2">40            {org.name}41            <PlanBadge plan={org.plan} />42            {org.suspended ? <StatusBadge status="suspended" /> : null}43            {org.providerVisibility ? <Badge variant="warning" dot>provider debug on</Badge> : null}44          </span>45        }46        description={47          <span className="inline-flex items-center gap-1">48            <Mono>{org.id}</Mono>49            <CopyButton value={org.id} size="icon-sm" className="size-6" />50            <span className="text-fg-subtle">· /{org.slug}</span>51          </span>52        }53        actions={54          <Button asChild variant="outline" size="sm">55            <Link href={`/admin/requests?org=${org.id}`}>Explore requests</Link>56          </Button>57        }58      />5960      {org.suspended ? <Alert variant="danger" title="Suspended" className="mb-4">API access is blocked for this organization.</Alert> : null}6162      <StatGrid cols={6} className="mb-6">63        <Stat label="Requests 30d" value={formatNumber(s.requests)} />64        <Stat label="Success" value={formatPercent(s.requests ? (s.successes / s.requests) * 100 : null)} />65        <Stat label="Revenue 30d" value={formatUsd(s.revenue)} />66        <Stat label="Upstream cost" value={formatUsd(s.cost)} />67        <Stat label="Margin" value={<MarginText value={margin} pct={s.revenue > 0 ? (margin / s.revenue) * 100 : null} />} />68        <Stat label="Bandwidth" value={formatBytes(s.bytes)} />69      </StatGrid>7071      <div className="grid gap-6 lg:grid-cols-2">72        <Panel title="Controls" description="Every change is written to the audit log with your user id.">73          <div className="grid gap-5">74            <div className="grid gap-1.5">75              <div className="text-[13px] font-medium">Plan</div>76              <div className="flex items-center gap-2">77                <Badge variant="solid">Unlimited</Badge>78                <span className="text-[12.5px] text-fg-muted">private platform — plans are not configurable</span>79              </div>80            </div>81            <OrgProviderVisibilitySwitch orgId={org.id} enabled={org.providerVisibility} />82            <OrgSuspendSwitch orgId={org.id} suspended={org.suspended} />83          </div>84        </Panel>85        <div className="grid gap-6">86          <Panel title="Spend limits">87            <OrgLimitsForm orgId={org.id} softLimitUsd={org.softLimitUsd} hardLimitUsd={org.hardLimitUsd} />88          </Panel>89          <Panel title="Details">90            <KV91              items={[92                { label: "Owner", value: d.owner ? <Link href={`/admin/users/${d.owner.id}`} className="hover:underline">{d.owner.email}</Link> : "—" },93                { label: "Stripe customer", value: org.stripeCustomerId ?? "not linked", mono: true },94                { label: "Created", value: formatDate(org.createdAt) },95                { label: "Updated", value: formatDate(org.updatedAt) },96              ]}97            />98          </Panel>99        </div>100      </div>101102      <Panel title="Members" flush className="mt-6">103        <Table>104          <TableHeader>105            <TableRow>106              <TableHead>User</TableHead>107              <TableHead>Role</TableHead>108              <TableHead className="text-right">Joined</TableHead>109            </TableRow>110          </TableHeader>111          <TableBody>112            {d.members.map((m) => (113              <TableRow key={m.userId}>114                <TableCell>115                  <Link href={`/admin/users/${m.userId}`} className="font-medium hover:underline">116                    {m.email}117                  </Link>118                  <span className="ml-2 text-fg-muted">{m.name}</span>119                </TableCell>120                <TableCell className="text-fg-muted">{m.role}</TableCell>121                <TableCell className="text-right">122                  <DateCell value={m.joinedAt} />123                </TableCell>124              </TableRow>125            ))}126          </TableBody>127        </Table>128      </Panel>129130      <div className="mt-6 grid gap-6 xl:grid-cols-2">131        <Panel title="Projects" flush>132          <Table>133            <TableHeader>134              <TableRow>135                <TableHead>Project</TableHead>136                <TableHead>Env</TableHead>137                <TableHead className="text-right">Keys</TableHead>138                <TableHead className="text-right">Req 30d</TableHead>139                <TableHead className="text-right">Created</TableHead>140              </TableRow>141            </TableHeader>142            <TableBody>143              {d.projects.length === 0 ? (144                <TableEmpty colSpan={5}>No projects.</TableEmpty>145              ) : (146                d.projects.map((p) => (147                  <TableRow key={p.id}>148                    <TableCell>149                      <Link href={`/admin/requests?project=${p.id}`} className="font-medium hover:underline">150                        {p.name}151                      </Link>152                      {p.archivedAt ? <Badge variant="outline" className="ml-2">archived</Badge> : null}153                      <div className="font-mono text-[11.5px] text-fg-subtle">{p.id}</div>154                    </TableCell>155                    <TableCell>156                      <Badge variant={p.environment === "development" ? "warning" : "success"} dot>157                        {p.environment}158                      </Badge>159                    </TableCell>160                    <TableCell className={numCell}>{p.keys}</TableCell>161                    <TableCell className={numCell}>{formatNumber(p.requests30d)}</TableCell>162                    <TableCell className="text-right">163                      <DateCell value={p.createdAt} />164                    </TableCell>165                  </TableRow>166                ))167              )}168            </TableBody>169          </Table>170        </Panel>171        <Panel title="API keys" description="Prefix and last 4 only. Plaintext keys are never stored." flush>172          <Table>173            <TableHeader>174              <TableRow>175                <TableHead>Key</TableHead>176                <TableHead>Project</TableHead>177                <TableHead>Mode</TableHead>178                <TableHead>Status</TableHead>179                <TableHead className="text-right">Last used</TableHead>180              </TableRow>181            </TableHeader>182            <TableBody>183              {d.keys.length === 0 ? (184                <TableEmpty colSpan={5}>No API keys.</TableEmpty>185              ) : (186                d.keys.map((k) => (187                  <TableRow key={k.id}>188                    <TableCell>189                      <div className="font-medium">{k.name}</div>190                      <Mono className="text-fg-muted">191                        {k.keyPrefix}…{k.last4}192                      </Mono>193                    </TableCell>194                    <TableCell className="text-fg-muted">{k.projectName ?? "—"}</TableCell>195                    <TableCell>196                      <StatusBadge status={k.mode} />197                    </TableCell>198                    <TableCell>{k.revokedAt ? <StatusBadge status="revoked" /> : k.expiresAt && k.expiresAt < new Date() ? <StatusBadge status="expired" /> : <StatusBadge status="active" />}</TableCell>199                    <TableCell className="text-right">200                      <DateCell value={k.lastUsedAt} />201                    </TableCell>202                  </TableRow>203                ))204              )}205            </TableBody>206          </Table>207        </Panel>208      </div>209210      <Panel title="Recent requests" description="Latest 25. Cost is upstream; price is what the customer is billed." flush className="mt-6">211        <Table>212          <TableHeader>213            <TableRow>214              <TableHead>Request</TableHead>215              <TableHead>Domain</TableHead>216              <TableHead>Status</TableHead>217              <TableHead>Network</TableHead>218              <TableHead>Country</TableHead>219              <TableHead className="text-right">Latency</TableHead>220              <TableHead className="text-right">Cost</TableHead>221              <TableHead className="text-right">Price</TableHead>222              <TableHead className="text-right">When</TableHead>223            </TableRow>224          </TableHeader>225          <TableBody>226            {d.recentRequests.length === 0 ? (227              <TableEmpty colSpan={9}>No requests yet.</TableEmpty>228            ) : (229              d.recentRequests.map((r) => (230                <TableRow key={r.id}>231                  <TableCell>232                    <Link href={`/admin/requests/${r.id}`} className="hover:underline">233                      <Mono>{r.id}</Mono>234                    </Link>235                  </TableCell>236                  <TableCell className="max-w-[220px] truncate">{r.domain}</TableCell>237                  <TableCell>238                    <StatusBadge status={r.status} />239                    {r.errorCode ? <span className="ml-1.5 font-mono text-[11.5px] text-fg-muted">{r.errorCode}</span> : null}240                  </TableCell>241                  <TableCell>242                    <NetworkBadge network={r.network} />243                  </TableCell>244                  <TableCell>245                    <Mono>{r.country ?? "—"}</Mono>246                  </TableCell>247                  <TableCell className={numCell}>{formatMs(r.latencyMs)}</TableCell>248                  <TableCell className={numCell}>{formatUsd(r.costUsd, true)}</TableCell>249                  <TableCell className={numCell}>{formatUsd(r.priceUsd, true)}</TableCell>250                  <TableCell className="text-right">251                    <DateCell value={r.createdAt} />252                  </TableCell>253                </TableRow>254              ))255            )}256          </TableBody>257        </Table>258      </Panel>259260      <Panel title="Audit log" description="Last 30 entries scoped to this organization." flush className="mt-6">261        <Table>262          <TableHeader>263            <TableRow>264              <TableHead>When</TableHead>265              <TableHead>Action</TableHead>266              <TableHead>Target</TableHead>267              <TableHead>Metadata</TableHead>268            </TableRow>269          </TableHeader>270          <TableBody>271            {d.audit.length === 0 ? (272              <TableEmpty colSpan={4}>No audit entries.</TableEmpty>273            ) : (274              d.audit.map((a) => (275                <TableRow key={a.id}>276                  <TableCell>277                    <DateCell value={a.createdAt} relative={false} />278                  </TableCell>279                  <TableCell>280                    <Badge variant={a.action === "admin.action" ? "danger" : "default"}>{a.action}</Badge>281                  </TableCell>282                  <TableCell>283                    <Mono>{a.target ?? "—"}</Mono>284                  </TableCell>285                  <TableCell className="max-w-[420px] truncate font-mono text-[12px] text-fg-muted" title={a.metadata ? JSON.stringify(a.metadata) : undefined}>286                    {a.metadata ? JSON.stringify(a.metadata) : "—"}287                  </TableCell>288                </TableRow>289              ))290            )}291          </TableBody>292        </Table>293      </Panel>294    </>295  );296}297